常见 Virtual Dom 面试题
vdom 是什么?为什么会存在 vdom?
Virtual Dom 也称虚拟 DOM ,即使用 JS 模拟 DOM 结构。
众所周知,操作 DOM 是很耗费性能的一件事情,既然如此,我们可以考虑通过 JS 对象来模拟 DOM 对象,毕竟操作 JS 对象比操作 DOM 省时的多。DOM 变化的对比,放到 JS 层来做,提高重绘性能。
举个例子:1
2
3
4<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>
上面是一段HTML代码,我们用 JS 来模拟一下它的 DOM 结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {
className: 'item'
},
children: ['Item 1']
},
{
tag: 'li',
attrs: {
className: 'item'
},
children: ['Item 2']
},
]
}
为什么要使用vdom?
设计一个需求场景:
- 将数据设计成一个表格。
- 随意修改一个信息,表格也随之改变。
使用 jQuery 来实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript">
var data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 渲染函数
function render(data) {
var $container = $('#container')
// 清空容器,重要!!!
$container.html('')
// 拼接 table
var $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
data.forEach(function (item) {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(function () {
data[1].age = 30
data[2].address = '深圳'
// re-render 再次渲染
render(data)
})
// 页面加载完立刻执行(初次渲染)
render(data)
</script>
在开发者工具可以看到,每一次修改都需要将整个表格推倒重来,重新渲染。而dom操作又是十分昂贵的,为了减少dom操作,需要用JS进行DOM对比,提高效率。
vdom 的如何应用,核心API是什么?
使用 snabbdom 改写上面的情景:
1 | <div id="container"></div> |
1 | <div id="container"></div> |
核心 API:
- h(‘<标签名>’, {…属性…}, […子属性…])
- h(‘<标签名>’, {…属性…}, ‘…’)
- patch(vontainer, vnode)
patch(vnode, newVnode)
介绍一下 diff 算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50function createElement(vnode) {
var tag = vnode.tag // 'ul'
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 给 elem 添加属性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加子元素
elem.appendChild(createElement(childVnode)) // 递归
})
// 返回真实的 DOM 元素
return elem
}
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 替换
replaceNode(childVnode, newChildVnode)
}
})
}
function replaceNode(vnode, newVnode) {
var elem = vnode.elem // 真实的 DOM 节点
var newElem = createElement(newVnode)
// 替换
}diff是 Linux的基础命令
- 为了找出需要更新的节点
- diff实现 patch(vontainer, vnode) patch(vnode, newVnode)
- 核心逻辑:createElement updateChildren